//
// Copyright (c) 2009 All Right Reserved
//
// vl
//
// 2009-01-01
// Contains ...
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using JetBrains.Annotations;
using LargoCommon.Music;
namespace LargoCommon.Midi {
///
/// Raw Midi Operations.
///
[UsedImplicitly]
public static class MidiOperations {
#region Format conversion
/// Converts a MIDI sequence from its current format to the specified format.
/// The sequence to be converted.
/// The format to which we want to convert the sequence.
/// Options used when doing the conversion.
/// The converted sequence.
///
/// This may or may not return the same sequence as passed in.
/// Regardless, the reference passed in should not be used after this call as the old
/// sequence could be unusable if a different reference was returned.
///
[UsedImplicitly]
public static CompactMidiStrip Convert(CompactMidiStrip sequence, int givenFormat, FormatConversionOptions options = FormatConversionOptions.None) {
// Validate the parameters
if (sequence == null) {
throw new ArgumentNullException(nameof(sequence));
}
if (givenFormat < 0 || givenFormat > 2) {
throw new ArgumentOutOfRangeException(nameof(givenFormat), givenFormat, "The format must be 0, 1, or 2.");
}
// Handle the simple cases
if (sequence.Format == givenFormat) {
return sequence;
} // already in requested format
if (givenFormat != 0 || sequence.NumberOfLines == 1) { // only requires change in format #
// Change the format and return the same sequence
sequence.Format = givenFormat;
return sequence;
}
// Now the hard one, converting to format 0.
// We need to combine all tracks into 1.
var newSequence = new CompactMidiStrip(givenFormat, sequence.Header.Division);
// Iterate through all events in all tracks and change deltaTimes to actual times.
// We'll then be able to sort based on time and change them back to deltas later
foreach (var track in sequence.Where(track => track != null && track.Events.Count > 0)) {
track.Events.RecomputeAbsoluteTimes(); //// ConvertDeltasToTotals();
}
var newTrack = NewTrackFromSequence(sequence, options);
newSequence.AddTrack(newTrack);
//// Return the new sequence
return newSequence;
}
#endregion
#region Transposition
/// Transposes a MIDI sequence up/down the specified number of half-steps.
/// The sequence to be transposed.
/// The number of steps up(+) or down(-) to transpose the sequence.
/// Whether drum sequence should also be transposed.
/// If the step value is too large or too small, notes may wrap.
[UsedImplicitly]
public static void Transpose(IEnumerable sequence, int steps, bool includeDrums = false) {
// If the sequence is null, throw an exception.
if (sequence == null) {
throw new ArgumentNullException(nameof(sequence));
}
// Modify each track // Modify each event
//// If the event is not a voice MIDI event but the channel is the
//// drum channel and the user has chosen not to include drums in the
//// transposition (which makes sense), skip this event.
//// If the event is a VoiceNoteOn, VoiceNoteOff, or Aftertouch, shift the note
//// according to the supplied number of steps.
foreach (var voiceEvent in from track in sequence
where track != null
from ev in track.Events
select ev as VoiceAbstractNote
into voiceEvent
where voiceEvent != null && (includeDrums || voiceEvent.Channel != MidiChannel.DrumChannel)
select voiceEvent) {
voiceEvent.Note = (byte)((voiceEvent.Note + steps) % 128);
}
}
#endregion
#region Trimming
/// Trims a MIDI file to a specified length.
/// The sequence to be copied and trimmed.
/// The requested time length of the new MIDI sequence.
/// A MIDI sequence with only those events that fell before the requested time limit.
[UsedImplicitly]
public static CompactMidiStrip Trim(CompactMidiStrip sequence, long totalTime) {
Contract.Requires(sequence != null);
if (sequence == null) {
return null;
}
//// Create a new sequence to mimic the old
var newSequence = new CompactMidiStrip(sequence.Format, sequence.Header.Division);
//// Copy each track up to the specified time limit
foreach (var track in sequence.Where(track => track != null && track.Events.Count > 0)) {
track.TrimTo(newSequence, totalTime);
}
//// Return the new sequence
return newSequence;
}
#endregion
///
/// New track from sequence.
///
/// The sequence.
/// The options.
/// New midi track.
private static MidiTrack NewTrackFromSequence(CompactMidiStrip sequence, FormatConversionOptions options) {
Contract.Requires(sequence != null);
var newTrack = new MidiTrack();
//// Add all events to new track (except for end of track markers!)
//// If this event has a channel, and if we're storing lines as channels, copy to it
//// Add all events, except for end of track markers (we'll add our own)
var trackNumber = 0;
foreach (var track in sequence.Where(track => track != null)) {
foreach (var midiEvent in track.Events) {
if ((options & FormatConversionOptions.CopyTrackToChannel) > 0
&& (midiEvent is VoiceEvent vev) && trackNumber <= 0xF) { //// && trackNumber >= 0
vev.Channel = (MidiChannel)(byte)trackNumber;
}
if (!(midiEvent is MetaEndOfTrack)) {
newTrack.Events.Add(midiEvent);
}
}
trackNumber++;
}
// Sort the events
newTrack.Events.SortByStartTime();
// Now go back through all of the events and update the times to be deltas
newTrack.Events.RecomputeDeltaTimes();
// Put an end of track on for good measure as we've already taken out
// all of the ones that were in the original sequence.
newTrack.Events.Add(new MetaEndOfTrack(0));
return newTrack;
}
}
}